<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8">
  <title>全微分引入演示</title>
  <style>
    body { margin: 0; background-color: #f0f0f0; font-family: sans-serif; }
    .header {
      background-color: #cce6ff;
      color: #003366;
      text-align: center;
      padding: 10px 0;
      font-size: 20px;
      font-weight: bold;
      border-bottom: 1px solid #99ccff;
      border-radius: 0 0 10px 10px;
      position: fixed;
      top: 0;
      width: 100%;
      z-index: 10;
    }
    #container { width: 100vw; height: 100vh; }
    .slider-panel {
      position: absolute;
      top: 60px;
      left: 20px;
      background: white;
      padding: 10px;
      border-radius: 8px;
      box-shadow: 0 0 10px rgba(0,0,0,0.2);
    }
  </style>
</head>
<body>
  <div class="header">全微分引入演示</div>
  <div id="container"></div>
  <div class="slider-panel">
    <label for="xSlider">Δx: <span id="dxVal">0.00</span></label><br>
    <input type="range" id="xSlider" min="-10" max="10" step="0.01" value="0"><br>
    <label for="ySlider">Δy: <span id="dyVal">0.00</span></label><br>
    <input type="range" id="ySlider" min="-10" max="10" step="0.01" value="0"><br>
    <p id="dzDisplay">Δf ≈ ?</p>
    <button id="switchFunc">切换函数</button>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
  <script>
    const container = document.getElementById('container');
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    const controls = new THREE.OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true;

    const gridHelper = new THREE.GridHelper(30, 30);
    scene.add(gridHelper);

    function createAxis(start, end, color, labelText, labelPosition) {
      const material = new THREE.LineBasicMaterial({ color });
      const points = [start, end];
      const geometry = new THREE.BufferGeometry().setFromPoints(points);
      const line = new THREE.Line(geometry, material);
      scene.add(line);

      const loader = new THREE.FontLoader();
      loader.load('https://threejs.org/examples/fonts/helvetiker_regular.typeface.json', function (font) {
        const textGeo = new THREE.TextGeometry(labelText, {
          font: font,
          size: 0.6,
          height: 0.05
        });
        const textMaterial = new THREE.MeshBasicMaterial({ color });
        const mesh = new THREE.Mesh(textGeo, textMaterial);
        mesh.position.copy(labelPosition);
        scene.add(mesh);
      });

      return line;
    }

    createAxis(new THREE.Vector3(-15, 0, 0), new THREE.Vector3(15, 0, 0), 0xff0000, 'x', new THREE.Vector3(15.5, 0.2, 0));
    createAxis(new THREE.Vector3(0, 0, -15), new THREE.Vector3(0, 0, 15), 0x00ff00, 'y', new THREE.Vector3(0.5, 0.2, 15));

    const geometry = new THREE.PlaneGeometry(20, 20, 100, 100);
    geometry.rotateX(-Math.PI / 2);

    function setSurfaceFunction(geometry, func) {
      const positionAttribute = geometry.attributes.position;
      for (let i = 0; i < positionAttribute.count; i++) {
        const x = positionAttribute.getX(i);
        const z = positionAttribute.getZ(i);
        const y = func(x, z);
        positionAttribute.setY(i, y);
      }
      positionAttribute.needsUpdate = true;
    }

    let funcIndex = 0;
    const functions = [
      (x, z) => 0.1 * (x * x + z * z),
      (x, z) => 0.05 * (x * x + z * z + x * z)
    ];

    setSurfaceFunction(geometry, functions[funcIndex]);

    const material = new THREE.MeshBasicMaterial({ color: 0x88ccff, wireframe: true });
    const surface = new THREE.Mesh(geometry, material);
    scene.add(surface);

    const pointGeometry = new THREE.SphereGeometry(0.2, 16, 16);
    const pointMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
    const point = new THREE.Mesh(pointGeometry, pointMaterial);
    scene.add(point);

    const whiteMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff });
    const projectionPoint = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(projectionPoint);
    const xProjPoint = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(xProjPoint);
    const yProjPoint = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(yProjPoint);
    const yXProjPoint = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(yXProjPoint);
    const finalProjPoint = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(finalProjPoint);
    const altProjFootY = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(altProjFootY);
    const altProjFootFinal = new THREE.Mesh(pointGeometry.clone(), whiteMaterial);
    scene.add(altProjFootFinal);

    const baseX = 0, baseZ = 0;
    let currentFunc = functions[funcIndex];
    let initialY = currentFunc(baseX, baseZ);
    const initialPoint = new THREE.Mesh(pointGeometry.clone(), pointMaterial.clone());
    initialPoint.position.set(baseX, initialY, baseZ);
    scene.add(initialPoint);

    const dxSlider = document.getElementById("xSlider");
    const dySlider = document.getElementById("ySlider");
    const dxVal = document.getElementById("dxVal");
    const dyVal = document.getElementById("dyVal");
    const dzDisplay = document.getElementById("dzDisplay");
    const switchFuncBtn = document.getElementById("switchFunc");

    let fullLine, verticalLine, partialLineX, partialLineY, altPartialLineY, altPartialLineX, altVerticalY, altVerticalFinal;

    function drawLine(start, end, existingLine, color = 0x000000) {
      const material = new THREE.LineBasicMaterial({ color });
      const points = [start, end];
      const geometry = new THREE.BufferGeometry().setFromPoints(points);
      const line = new THREE.Line(geometry, material);
      if (existingLine) scene.remove(existingLine);
      scene.add(line);
      return line;
    }

    function updatePoint() {
      const dx = parseFloat(dxSlider.value);
      const dy = parseFloat(dySlider.value);
      dxVal.innerText = dx.toFixed(2);
      dyVal.innerText = dy.toFixed(2);

      const newX = baseX + dx;
      const newZ = baseZ + dy;
      const newY = currentFunc(newX, newZ);
      point.position.set(newX, newY, newZ);

      projectionPoint.position.set(newX, 0, newZ);

      const fX = currentFunc(newX, baseZ);
      const fY = currentFunc(newX, newZ);
      const fZ = currentFunc(baseX, newZ);

      xProjPoint.position.set(newX, fX, baseZ);
      yProjPoint.position.set(newX, fX, newZ);
      yXProjPoint.position.set(baseX, fZ, newZ);
      finalProjPoint.position.set(newX, newY, newZ);

      altProjFootY.position.set(baseX, 0, newZ);
      altProjFootFinal.position.set(newX, 0, newZ);

      const deltaF = newY - initialY;
      dzDisplay.innerText = `Δf = ${deltaF.toFixed(4)}`;

      const baseVec = new THREE.Vector3(baseX, initialY, baseZ);
      const fullVec = new THREE.Vector3(newX, newY, newZ);
      const verticalVec = new THREE.Vector3(newX, 0, newZ);
      const xStepVec = new THREE.Vector3(newX, fX, baseZ);
      const yStepVec = new THREE.Vector3(newX, fX, newZ);
      const yOnlyVec = new THREE.Vector3(baseX, fZ, newZ);

      fullLine = drawLine(baseVec, fullVec, fullLine, 0x000000);
      verticalLine = drawLine(fullVec, verticalVec, verticalLine, 0x888888);
      partialLineX = drawLine(baseVec, xStepVec, partialLineX, 0x0000ff);
      partialLineY = drawLine(xStepVec, yStepVec, partialLineY, 0x00ffff);
      altPartialLineY = drawLine(baseVec, yOnlyVec, altPartialLineY, 0xff00ff);
      altPartialLineX = drawLine(yOnlyVec, fullVec, altPartialLineX, 0xffaa00);
      altVerticalY = drawLine(yOnlyVec, altProjFootY.position, altVerticalY, 0xaaaaaa);
      altVerticalFinal = drawLine(fullVec, altProjFootFinal.position, altVerticalFinal, 0xbbbbbb);
    }

    switchFuncBtn.addEventListener("click", () => {
      funcIndex = (funcIndex + 1) % functions.length;
      currentFunc = functions[funcIndex];
      setSurfaceFunction(geometry, currentFunc);
      initialY = currentFunc(baseX, baseZ);
      initialPoint.position.set(baseX, initialY, baseZ);
      updatePoint();
    });

    dxSlider.addEventListener("input", updatePoint);
    dySlider.addEventListener("input", updatePoint);

    camera.position.set(10, 15, 10);
    camera.lookAt(0, 0, 0);

    function animate() {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }

    animate();
    updatePoint();
  </script>
</body>
</html>
